package net.posick.ws.soap;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.posick.tree.TraverseAction;
import net.posick.ws.Constants;
import net.posick.ws.xml.Name;
import net.posick.ws.xml.XmlElement;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.RequestLine;
import org.apache.http.entity.StringEntity;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.util.EntityUtils;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

public class SOAPRequestHandler implements HttpRequestHandler
{
    public static class HttpResponder extends BroadcastReceiver
    {
        private XmlElement response;
        
        private Throwable fault;
        
        
        protected HttpResponder()
        {
        }
        
        
        protected XmlElement getResponse()
        {
            return response;
        }
        
        
        protected boolean wasResponseReceived()
        {
            return response != null || fault != null;
        }
        
        
        public Throwable getFault()
        {
            return fault;
        }


        @Override
        public void onReceive(Context context, Intent intent)
        {
            Bundle bundle = getResultExtras(true);
            
            Object o = bundle.getSerializable(Constants.EXTRA_RESPONSE);
            Object t = bundle.getSerializable(Constants.EXTRA_SOAP_FAULT);
            
            Log.i(LOG_TAG, "----- " + getClass().getSimpleName() + " received Response for Intent \"" + intent.getAction() + "\" [" + bundle + "] - Response = " + o + " -----");
            Log.i(LOG_TAG, "----- Intent Extras:");
            for (String key : bundle.keySet())
            {
                Object value = bundle.get(key);
                Log.i(LOG_TAG, "-----     Entry -> \"" + key + "\"=\"" + value + "\" of type " + (value == null ? "null" : value.getClass()));
            }
            
            try
            {
                if (t != null && t instanceof Throwable)
                {
                    Log.i(LOG_TAG, "----- Fault Received: " + t);
                    this.fault = (Throwable) t;
                } else if (o != null && o instanceof XmlElement)
                {
                    Log.i(LOG_TAG, "----- Response Received: " + o);
                    this.response = (XmlElement) o;
                } else
                {
                    Log.e(LOG_TAG, "No Value in response Received [t=\"" + (t != null ? t.getClass() : "null") + "\", o=\"" + (o != null ? o.getClass() : "null") + "\"]");
                }
            } catch (Exception e)
            {
                Log.e(LOG_TAG, "Error processing response - " + e.getMessage(), e);
            }
            
            synchronized(this)
            {
                this.notifyAll();
            }
        }
    }


    public static final String LOG_TAG = Constants.LOG_TAG + "." + SOAPRequestHandler.class.getSimpleName();

    public static final Name MESSAGE_ID = new Name("MessageID");
    
    private String pattern;
    
    private Map<String, Intent> actionMap = new HashMap<String, Intent>();
    
    private String url;
    
    private String string;
    
    
    public SOAPRequestHandler(String pattern, String action, Intent intent)
    {
        this.pattern = pattern;
        this.actionMap.put(action, intent);
        this.string = getClass().getSimpleName() + "[action=\"" + (action == null ? "" : action) + "\", pattern=\"" + (pattern == null ? "" : pattern) + "\"]";
    }
    
    
    @Override
    public void handle(final HttpRequest request, final HttpResponse response, final HttpContext context)
    throws HttpException, IOException
    {
        try
        {
            Set<CharSequence> actions = new LinkedHashSet<CharSequence>();
            String messageId = null;;
            Header[] headers = request.getAllHeaders();
            for (Header header : headers)
            {
                for (HeaderElement element : header.getElements())
                {
                    String name = element.getName();
                    if (name != null && (name.equals(SOAPAction.HTTP_HEADER_NAME) || name.endsWith("-" + SOAPAction.HTTP_HEADER_NAME)))
                    {
                        actions.add(header.getValue());
                    }
                }
            }
            
            RequestLine requestLine = request.getRequestLine();
            String uri = requestLine.getUri();
            String method = requestLine.getMethod();
            ProtocolVersion protocolVersion = requestLine.getProtocolVersion();
            
            String raw = null;
            if (request instanceof HttpEntityEnclosingRequest)
            {
                HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
                raw = EntityUtils.toString(entity);
            }
            
            Log.i(LOG_TAG, getClass().getSimpleName() + " request (" + request.getClass().getCanonicalName() + ") [" + method + "] \"" + uri + "\" - \"" + (raw != null ? raw : "No Message") + "\"");
            
            XmlElement message = null;
            SOAPEnvelope envelope = null;
            if (raw != null)
            {
                try
                {
                    envelope = SOAPEnvelope.parse(raw);
                } catch (Exception e)
                {
                    Log.e(LOG_TAG, "Error parsing SOAP Message - \"" + e.getMessage() + "\"!", e);
                }
            }
            
            Context ctx = (Context) context.getAttribute(Constants.ANDROID_CONTEXT);
            List<XmlElement> requests = new ArrayList<XmlElement>();
            
            if (envelope != null)
            {
                SOAPBody body = envelope.getBody();
                if (body != null)
                {
                    List<XmlElement> elements = envelope.getHeaders(SOAPAction.FQN);
                    for (XmlElement element : elements)
                    {
                        CharSequence chars = element.getValue();
                        if (chars != null)
                        {
                            actions.add(chars);
                        }
                    }
                    
                    requests.addAll(body.getChildren());
                    
                    /* TODO: Add MessageID
                    elements = envelope.getHeaders(MESSAGE_ID);
                    for (XmlElement element : elements)
                    {
                        CharSequence chars = element.getValue();
                        if (chars != null)
                        {
                            messageId = chars.toString();
                        }
                    }
                    */
                }
            }
            XmlElement[] requestsArray = requests.toArray(new XmlElement[requests.size()]);
            
            if (actions.size() == 0)
            {
                actions.add("");
            }
            
            for (CharSequence action : actions)
            {
                Intent srcIntent = actionMap.get(action);
                if (srcIntent != null)
                {
                    Intent intent = srcIntent.cloneFilter().addCategory(Constants.CATEGORY_ENDPOINT_CALLBACK)
                                            .setType(Constants.MEDIA_TYPE_SOAP)
                                            .putExtra(Constants.EXTRA_URI_PATTERN, uri)
                                            .putExtra(Constants.EXTRA_SOAP_ACTION, action)
                                            .putExtra(Constants.EXTRA_METHOD, method)
                                            .putExtra(Constants.EXTRA_PROTOCOL, protocolVersion.getProtocol())
                                            .putExtra(Constants.EXTRA_PROTOCOL_VERSION, protocolVersion.toString());
    
                    for (String cat : srcIntent.getCategories())
                    {
                        intent.addCategory(cat);
                    }
                    
                    if (raw != null)
                    {
                        intent = intent.putExtra(Constants.EXTRA_RAW_MESSAGE, raw);
                    }
                    
                    if (message != null)
                    {
                        intent = intent.putExtra(Constants.EXTRA_MESSAGE, message);
                    }
                    
                    if (requestsArray != null && requestsArray.length > 0)
                    {
                        intent = intent.putExtra(Constants.EXTRA_REQUEST, requestsArray);
                    }
                    
                    Log.i(LOG_TAG, getClass().getSimpleName() + " broadcasting Intent \"" + intent.getAction() + "\" [" + intent + "] to context \"" + ctx.getPackageName() + "\" [" + ctx + "]");
                    
                    HttpResponder responder = new HttpResponder();
                    ctx.sendOrderedBroadcast(intent.addCategory(Constants.CATEGORY_ENDPOINT_CALLBACK), Constants.PERMISSION_REGISTER_ENDPOINT, responder, null, Activity.RESULT_OK, null, null);
                    
                    // Wait for response from registered BroadcastReceivers
                    synchronized (responder)
                    {
                        long now;
                        long start = System.currentTimeMillis();
                        long endTime = start + 30000;
                        Log.i(LOG_TAG, getClass().getSimpleName() + " waiting for response.");
                        while ((now = System.currentTimeMillis()) < endTime && !responder.wasResponseReceived())
                        {
                            try
                            {
                                responder.wait(10);
                            } catch (InterruptedException e)
                            {
                                Log.i(LOG_TAG, getClass().getSimpleName() + " Interrupted");
                            }
                        }
                        Log.w(LOG_TAG, getClass().getSimpleName() + " No response, waited for " + (double) ((double) (now - start) / (double) 1000) + "s.");
                        
                        Throwable t = responder.getFault();
                        XmlElement soapResponse = responder.getResponse();
                        if (t != null)
                        {
                            Log.i(LOG_TAG, getClass().getSimpleName() + " fault received.");
                            Log.i(LOG_TAG, "----- " + getClass().getSimpleName() + " writing fault \"" + new SOAPFault(t) + "\" to HttpResponse -----");
                            
                            response.setStatusCode(400);
                            try
                            {
                                String soapAction = responder.getResultExtras(true).getString(Constants.EXTRA_SOAP_ACTION_RESPONSE);
                                StringEntity entity = new StringEntity(wrapResponse(soapAction, t).toString());
                                entity.setContentType(intent.getType());
                                response.setEntity(entity);
                            } catch (UnsupportedEncodingException e)
                            {
                                Log.i(LOG_TAG, "----- " + getClass().getSimpleName() + " Exception while writing fault \"" + new SOAPFault(t) + "\" to HttpResponse -----");
                                response.setStatusCode(400);
                                response.setReasonPhrase(e.getMessage());
                            }
                        } else if (soapResponse != null)
                        {
                            Log.i(LOG_TAG, getClass().getSimpleName() + " response received.");
                            Log.i(LOG_TAG, "----- " + getClass().getSimpleName() + " writing response \"" + soapResponse + "\" to HttpResponse -----");
                            
                            response.setStatusCode(200);
                            try
                            {
                                String soapAction = responder.getResultExtras(true).getString(Constants.EXTRA_SOAP_ACTION_RESPONSE);
                                StringEntity entity = new StringEntity(wrapResponse(soapAction, messageId, soapResponse).toString());
                                entity.setContentType(intent.getType());
                                response.setEntity(entity);
                            } catch (UnsupportedEncodingException e)
                            {
                                Log.i(LOG_TAG, "----- " + getClass().getSimpleName() + " Exception while writing response \"" + new SOAPFault(t) + "\" to HttpResponse -----");
                                response.setStatusCode(400);
                                response.setReasonPhrase(e.getMessage());
                            }
                        } else
                        {
                            response.setStatusCode(400);
                            response.setReasonPhrase("No response from registered application");
                        }
                    }
                } else
                {
                    Log.i(LOG_TAG, "Received HTTP Request for an unregistered Web Service [uri=\"" + uri + "\", action=\"" + action + "\"].");
                }
            }
        } catch (Exception e)
        {
            Log.e(LOG_TAG, "Error processing HTTP Request - " + e.getMessage(), e);
            response.setStatusCode(500);
            response.setReasonPhrase(e.getMessage());
        }
    }
    

    protected static SOAPEnvelope wrapResponse(String soapAction, String messageID, XmlElement element)
    {
        SOAPBody body = null;
        try
        {
             body = new SOAPBody(element);
        } catch (SOAPException e)
        {
            body = new SOAPBody(new SOAPFault(e));
        }
        
        return new SOAPEnvelope(soapAction != null && soapAction.length() > 0 ? new SOAPHeader(new SOAPAction(soapAction), messageID != null ? new SOAPMessageID(messageID) : null) : null, body);
    }
    

    protected static SOAPEnvelope wrapResponse(String soapAction, Throwable t)
    {
        return new SOAPEnvelope(soapAction != null && soapAction.length() > 0 ? new SOAPHeader(new SOAPAction(soapAction)) : null, new SOAPBody(t));
    }
    
    
    protected List<CharSequence> extractSOAPAction(XmlElement message)
    {
        final List<CharSequence> actions = new ArrayList<CharSequence>();

        message.traverse(new TraverseAction<XmlElement>()
        {
            Name actionName = new Name(SOAPAction.ELEMENT_NAME);
            
            @Override
            public int onNode(XmlElement node, int level)
            {
                if (actionName.equals(node.getName()) && SOAPAction.NAMESPACE_SOAP_ADDRESSING.equals(actionName.getNamespace().getUri()))
                {
                    actions.add(node.getValue());
                    return TraverseAction.STOP_TREE;
                }
                return TraverseAction.CONTINUE;
            }
        });
        
        return actions.size() > 0 ? actions : null;
    }
    
    
    protected List<XmlElement> extractSOAPBodyPayload(XmlElement message)
    {
        final List<XmlElement> payload = new ArrayList<XmlElement>();
        
        message.traverse(new TraverseAction<XmlElement>()
        {
            Name bodyName = new Name(SOAPBody.ELEMENT_NAME);
            
            @Override
            public int onNode(XmlElement node, int level)
            {
                Name name = node.getName();
                if (bodyName.equals(name) && SOAPBody.NAMESPACE_SOAP_1_1.equals(name.getNamespace().getUri()))
                {
                    payload.addAll(node.getChildren());
                    return TraverseAction.STOP_TREE;
                }
                return TraverseAction.CONTINUE;
            }
        });
        
        return payload;
    }


    public String getPattern()
    {
        return pattern;
    }
    
    
    public Set<String> getActions()
    {
        return actionMap.keySet();
    }
    
    
    public Collection<Intent> getIntents()
    {
        return actionMap.values();
    }
    
    
    public void putIntent(String action, Intent intent)
    {
        this.actionMap.put(action, intent);
    }
    
    
    public Intent getIntent(String action)
    {
        return this.actionMap.get(action);
    }


    public Intent removeIntent(String action)
    {
        return this.actionMap.remove(action);
    }


    public boolean hasIntents()
    {
        return actionMap.size() > 0;
    }


    public String getUrl()
    {
        return url;
    }


    public void setUrl(String url)
    {
        this.url = url;
    }


    @Override
    public int hashCode()
    {
        return string.hashCode();
    }


    @Override
    public boolean equals(Object o)
    {
        if (o instanceof SOAPRequestHandler)
        {
            SOAPRequestHandler that = (SOAPRequestHandler) o;
            Set<String> thisActions = actionMap.keySet();
            Set<String> thatActions = that.actionMap.keySet();
            
            return (thisActions.size() == thatActions.size() && (thisActions.size() == 0 || thisActions.containsAll(thatActions))) && 
                   (this.pattern == that.pattern || (this.pattern != null && this.pattern.equals(that.pattern)));
        }
        
        return false;
    }


    @Override
    public String toString()
    {
        return string;
    }
}
